-- Load shared/clientside stuff
AddCSLuaFile("cl_init.lua")
AddCSLuaFile("shared.lua")
include("shared.lua")

ENT.WireDebugName = "ZCPU"


--------------------------------------------------------------------------------
function ENT:Initialize()
  -- Physics properties
  self:PhysicsInit(SOLID_VPHYSICS)
  self:SetMoveType(MOVETYPE_VPHYSICS)
  self:SetSolid(SOLID_VPHYSICS)

  -- Inputs/outputs
  self.Inputs = Wire_CreateInputs(self.Entity, { "MemBus", "IOBus", "Frequency", "Clk", "Reset", "Interrupt"})
  self.Outputs = Wire_CreateOutputs(self.Entity, { "Error" })

  -- CPU platform settings
  self.Clk = 0
  self.Frequency = 2000

  -- Create virtual machine
  self.VM = CPULib.VirtualMachine()
  self.VM.SerialNo = CPULib.GenerateSN("CPU")
  self.VM:Reset()
  
  self:SetCPUName()
  self:SetMemoryModel("64krom")
  self.VM.SignalError = function(VM,errorCode)
    Wire_TriggerOutput(self, "Error", errorCode)
  end
  self.VM.SignalShutdown = function(VM)
    self.Clk = 0
  end
  self.VM.ExternalWrite = function(VM,Address,Value)
    if Address >= 0 then -- Use MemBus
      local MemBusSource = self.Inputs.MemBus.Src
      if MemBusSource then
        if MemBusSource.ReadCell then
          local result = MemBusSource:WriteCell(Address-self.VM.RAMSize,Value)
          if result then return true
          else VM:Interrupt(7,Address) return false end
        else VM:Interrupt(8,Address) return false end
      else VM:Interrupt(7,Address) return false end
    else -- Use IOBus
      local IOBusSource = self.Inputs.IOBus.Src
      if IOBusSource then
        if IOBusSource.ReadCell then
          local result = IOBusSource:WriteCell(-Address-1,Value)
          if result then return true
          else VM:Interrupt(10,-Address-1) return false end
        else VM:Interrupt(8,Address+1) return false end
      else return true end
    end
  end
  self.VM.ExternalRead = function(VM,Address)
    if Address >= 0 then -- Use MemBus
      local MemBusSource = self.Inputs.MemBus.Src
      if MemBusSource then
        if MemBusSource.ReadCell then
          local result = MemBusSource:ReadCell(Address-self.VM.RAMSize)
          if result then return result
          else VM:Interrupt(7,Address) return end
        else VM:Interrupt(8,Address) return end
      else VM:Interrupt(7,Address) return end
    else -- Use IOBus
      local IOBusSource = self.Inputs.IOBus.Src
      if IOBusSource then
        if IOBusSource.ReadCell then
          local result = IOBusSource:ReadCell(-Address-1)
          if result then return result
          else VM:Interrupt(10,-Address-1) return end
        else VM:Interrupt(8,Address+1) return end
      else return 0 end
    end
  end
  
  -- Player that debugs the processor
  self.DebuggerPlayer = nil
end


--------------------------------------------------------------------------------
-- Highspeed link support
--------------------------------------------------------------------------------
function ENT:ReadCell(Address)
  return self.VM:ReadCell(Address)
end

function ENT:WriteCell(Address,Value)
  return self.VM:WriteCell(Address,tonumber(Value) or 0)
end


--------------------------------------------------------------------------------
-- Set memory model
--------------------------------------------------------------------------------
local memoryModels = {
  ["64krom"]  = {  65536, 65536  },
  ["64k"]     = {  65536, 0      },
  ["128krom"] = { 131072, 131072 },
  ["128rom"]  = {      0, 128    },
  ["128"]     = {    128, 128    },
  ["flat"]    = {      0, 0      },
}

function ENT:SetMemoryModel(model)
  self.VM.RAMSize = memoryModels[model][1] or 65536
  self.VM.ROMSize = memoryModels[model][2] or 65536
end


--------------------------------------------------------------------------------
-- Execute ZCPU virtual machine
--------------------------------------------------------------------------------
function ENT:Run()
  -- Do not run if debugging is active
  if self.DebuggerPlayer then return end

  -- Calculate time-related variables
  local CurrentTime = CurTime()
  local DeltaTime = math.min(1/30,CurrentTime - (self.PreviousTime or 0))
  self.PreviousTime = CurrentTime

  -- Check if need to run till specific instruction
  if self.BreakpointInstructions then
    self.VM.TimerDT = DeltaTime
    self.VM.CPUIF = self
    self.VM:Step(8,function(self)
--      self:Emit("VM.IP = "..(self.PrecompileIP or 0))
--      self:Emit("VM.XEIP = "..(self.PrecompileTrueXEIP or 0))
        
      self:Dyn_Emit("if (VM.CPUIF.Clk == 1) and (VM.CPUIF.OnVMStep) then")
        self:Dyn_EmitState()
        self:Emit("VM.CPUIF.OnVMStep()")
      self:Emit("end")
      self:Emit("if VM.CPUIF.BreakpointInstructions[VM.IP] then")
        self:Dyn_EmitState()
        self:Emit("VM.CPUIF.OnBreakpointInstruction(VM.IP)")
        self:Emit("VM.CPUIF.Clk = 0")
        self:Emit("VM.TMR = VM.TMR + "..self.PrecompileInstruction)
        self:Emit("VM.CODEBYTES = VM.CODEBYTES + "..self.PrecompileBytes)
        self:Emit("if true then return end")
      self:Emit("end")
      self:Emit("if VM.CPUIF.LastInstruction and ((VM.IP > VM.CPUIF.LastInstruction) or VM.CPUIF.ForceLastInstruction) then")
        self:Dyn_EmitState()
        self:Emit("VM.CPUIF.ForceLastInstruction = nil")
        self:Emit("VM.CPUIF.OnLastInstruction()")
        self:Emit("VM.CPUIF.Clk = 0")
        self:Emit("VM.TMR = VM.TMR + "..self.PrecompileInstruction)
        self:Emit("VM.CODEBYTES = VM.CODEBYTES + "..self.PrecompileBytes)
        self:Emit("if true then return end")
      self:Emit("end")
    end)
    self.VM.CPUIF = nil
  else
    -- How many steps VM must make to keep up with execution
    local Cycles = math.max(1,math.floor(self.Frequency*DeltaTime*0.5))
    self.VM.TimerDT = (DeltaTime/Cycles)
  
    while (Cycles > 0) and (self.Clk > 0) and (self.VM.Idle == 0) do
      -- Run VM step
      local previousTMR = self.VM.TMR
      self.VM:Step()
      Cycles = Cycles - (self.VM.TMR - previousTMR)
    end
  end

  -- Update VM timer
  self.VM.TIMER = self.VM.TIMER + DeltaTime
  
  -- Reset idle register
  self.VM.Idle = 0
end


--------------------------------------------------------------------------------
-- Think loop
--------------------------------------------------------------------------------
function ENT:Think()
  self:Run()
  if self.Clk >= 1.0 then self.Entity:NextThink(CurTime()) end
  return true
end


--------------------------------------------------------------------------------
-- Write data to RAM and then flash ROM if required
--------------------------------------------------------------------------------
function ENT:FlashData(data)
  self.VM:Reset()
  for k,v in pairs(data) do
    self.VM:WriteCell(k,tonumber(v) or 0)
    if (k >= 0) and (k < self.VM.ROMSize) then
      self.VM.ROM[k] = tonumber(v) or 0
    end
  end
end


--------------------------------------------------------------------------------
-- Update CPU name
--------------------------------------------------------------------------------
function ENT:SetCPUName(name)
  local overlayStr = ""
  if name and (name ~= "") then
    self:SetOverlayText(string.format("%s\nS/N %d",name,self.VM.SerialNo))
  else
    self:SetOverlayText(string.format("Zyelios CPU\nS/N %d",self.VM.SerialNo))
  end
  self.CPUName = name
end


--------------------------------------------------------------------------------
-- Write advanced dupe
--------------------------------------------------------------------------------
function ENT:BuildDupeInfo()
  local info = self.BaseClass.BuildDupeInfo(self) or {}

  info.SerialNo = self.VM.SerialNo
  info.InternalRAMSize = self.VM.RAMSize
  info.InternalROMSize = self.VM.ROMSize
  info.CPUName         = self.CPUName

  if self.VM.ROMSize > 0 then
    info.Memory = {}
    for k,v in pairs(self.VM.ROM) do if v ~= 0 then info.Memory[k] = v end end
  end

  return info
end


--------------------------------------------------------------------------------
-- Read from advanced dupe
--------------------------------------------------------------------------------
function ENT:ApplyDupeInfo(ply, ent, info, GetEntByID)
  -- Compatibility with old NMI input
  if info.Wires and info.Wires.NMI then
    info.Wires.Interrupt = info.Wires.NMI
    info.Wires.NMI = nil
  end

  self.BaseClass.ApplyDupeInfo(self, ply, ent, info, GetEntByID)

  self.VM.SerialNo = info.SerialNo or CPULib.GenerateSN("UNK")
  self.VM.RAMSize  = info.InternalRAMSize or 65536
  self.VM.ROMSize  = info.InternalROMSize or 65536
  self:SetCPUName(info.CPUName)
  
  if info.Memory then--and
     --(((info.UseROM) and (info.UseROM == true)) or
     -- ((info.InternalROMSize) and (info.InternalROMSize > 0))) then
    self.VM.ROM = {}
    for k,v in pairs(info.Memory) do self.VM.ROM[k] = tonumber(v) or 0 end
    self.VM:Reset()
  end
end


--------------------------------------------------------------------------------
-- Handle external input
--------------------------------------------------------------------------------
function ENT:TriggerInput(iname, value)
      if iname == "Clk" then
    self.Clk = value
    if self.Clk >= 1.0 then self.Entity:NextThink(CurTime()) end
  elseif iname == "Frequency" then
    if (not SinglePlayer()) and (value > 1400000) then self.Frequency = 1400000 return end
    if value > 0 then self.Frequency = math.floor(value) end
  elseif iname == "Reset" then   --VM may be nil
    if self.VM.HWDEBUG ~= 0 then
      self.VM.DBGSTATE = math.floor(value)
      if (value > 0) and (value <= 1.0) then self.VM:Reset() end
    else
      if value >= 1.0 then self.VM:Reset() end
    end
    Wire_TriggerOutput(self, "Error", 0)
  elseif iname == "Interrupt" then
    if (value >= 32) && (value < 256) then
--      if (self.Clk >= 1.0) then self:NMIInterrupt(math.floor(value)) end
    end
  end
end
